昨天我們深入了解了斷言的各種用法,今天要學習 TDD 的精髓 —「紅綠重構循環」。
想像一下,你接到一個需求:「我們需要一個判斷質數的函數。」以前你可能直接開始寫程式,但現在我們要用 TDD 的方式:先寫測試(紅燈),再寫最簡實作(綠燈),最後改善代碼(重構)。
今天結束後,你將學會:
TDD 的核心是一個簡單而強大的三步循環:
🔴 紅燈(Red) ➜ 🟢 綠燈(Green) ➜ 🔵 重構(Refactor)
↑ ↓
← ← ← ← ← ← ← ← ← ← ← ← ← ← ←
紅燈階段的核心思想:先思考需求,再動手寫程式
建立 tests/day03/test_math_utils.py
:
"""數學工具庫測試"""
def test_identifies_small_prime_numbers():
"""識別小質數"""
# 還沒有 is_prime 函數,所以這個測試會失敗(紅燈)
assert is_prime(2) == True
assert is_prime(3) == True
assert is_prime(5) == True
def test_identifies_small_composite_numbers():
"""識別小合數"""
assert is_prime(4) == False
assert is_prime(6) == False
assert is_prime(9) == False
執行測試:
pytest tests/day03/ -v
預期結果:測試失敗,因為 is_prime
函數還不存在。這就是我們要的「紅燈」!
綠燈階段的核心思想:用最簡單的方法讓測試通過
建立 src/math/math_utils.py
:
"""數學工具庫"""
def is_prime(n: int) -> bool:
"""判斷是否為質數"""
# 最簡單的實作:硬編碼我們測試的數字
if n in [2, 3, 5]:
return True
if n in [4, 6, 9]:
return False
return False # 其他數字先回傳 False
更新測試檔,加入 import:
"""數學工具庫測試"""
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../../src'))
from math.math_utils import is_prime
def test_identifies_small_prime_numbers():
"""識別小質數"""
assert is_prime(2) == True
assert is_prime(3) == True
assert is_prime(5) == True
def test_identifies_small_composite_numbers():
"""識別小合數"""
assert is_prime(4) == False
assert is_prime(6) == False
assert is_prime(9) == False
執行測試:
pytest tests/day03/ -v
結果:測試通過!我們達到了綠燈階段。
重構階段的核心思想:在測試保護下,改善代碼品質
我們的硬編碼實作太醜了,讓我們重構:
"""數學工具庫"""
def is_prime(n: int) -> bool:
"""判斷是否為質數"""
# 處理邊界情況
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
# 檢查奇數因子到 sqrt(n)
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2
return True
執行測試確認重構成功:
pytest tests/day03/ -v
測試仍然通過!重構成功。
讓我們做第二輪循環,增加邊界情況的測試:
def test_handles_boundary_cases():
"""處理邊界情況"""
assert is_prime(0) == False
assert is_prime(1) == False
assert is_prime(-1) == False
def test_handles_larger_prime_numbers():
"""處理較大質數"""
assert is_prime(11) == True
assert is_prime(13) == True
執行測試 - 全部通過!因為我們的重構實作已經正確處理了這些情況。
TDD 不只是技術,更是一種開發節奏:
看到這些「代碼異味」就該重構了:
重構前:
if age >= 18:
# ...
重構後:
MIN_ADULT_AGE = 18
if age >= MIN_ADULT_AGE:
# ...
重構前:
def calc(x: float) -> float:
return x * 0.1
重構後:
def calculate_tax(price: float) -> float:
TAX_RATE = 0.1
return price * TAX_RATE
pytest 讓 TDD 變得更簡潔、更直觀:
assert result == expected
assert len(items) > 0
assert "error" in response
> assert is_prime(4) == True
E assert False == True
E + where False = is_prime(4)
與傳統測試框架相比,pytest 的語法更像在描述需求而非寫程式碼。這讓我們在 TDD 的紅燈階段更容易專注於「需求是什麼」,而不是「怎麼寫測試」。
完整 src/math/math_utils.py
:
"""數學工具庫"""
def is_prime(n: int) -> bool:
"""判斷是否為質數"""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2
return True
完整 tests/day03/test_math_utils.py
:
"""數學工具庫測試"""
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), '../../src'))
from math.math_utils import is_prime
def test_identifies_small_prime_numbers():
"""識別小質數"""
assert is_prime(2) == True
assert is_prime(3) == True
assert is_prime(5) == True
def test_identifies_small_composite_numbers():
"""識別小合數"""
assert is_prime(4) == False
assert is_prime(6) == False
assert is_prime(9) == False
def test_handles_boundary_cases():
"""處理邊界情況"""
assert is_prime(0) == False
assert is_prime(1) == False
assert is_prime(-1) == False
def test_handles_larger_prime_numbers():
"""處理較大質數"""
assert is_prime(11) == True
assert is_prime(13) == True
TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。
TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。
試著用 TDD 方式實作一個 is_even
函數:
記住 TDD 的節奏:紅燈 → 綠燈 → 重構,小步快跑!
明天我們將學習「測試結構和組織」,了解如何讓測試更清晰、更好維護。